require 'rexml/document'

class OfxHeader
  attr_reader(
    :ofx_header,
    :data,
    :version,
    :security,
    :encoding,
    :charset,
    :compression,
    :old_file_uid,
    :new_file_uid
  ) 
  
  def initialize(headers)
    headers.each do |h|
      key, value = h.split(":")
      case key.downcase
      when 'ofxheader'
        @ofx_header = value
      when 'data'
        @data = value
      when 'version'
        @version = value
      when 'security'
        @security = value
      when 'encoding'
        @encoding = value
      when 'charset'
        @charset = value
      when 'compression'
        @compression = value
      when 'oldfileuid'
        @old_file_uid = value
      when 'newfileuid'
        @new_file_uid = value
      else
        throw "Unknown field '#{key}' in OFX header"
      end
    end
  end
end

class OfxSignonResponse
  attr_reader(
    :status_code,
    :status_severity,
    :response_time,
    :response_language,
    :institution_name,
    :institution_id
  )

  def initialize(response)
    @status_code = REXML::XPath.first(response, "STATUS/CODE").text.strip
    raise "Invalid OFX response code (ERROR)" unless '0' == @status_code
    @status_severity = REXML::XPath.first(response, "STATUS/SEVERITY").text.strip
    @response_time = OfxUtil.parse_date(REXML::XPath.first(response, "DTSERVER").text.strip)
    @response_language = REXML::XPath.first(response, "LANGUAGE").text.strip
    x = REXML::XPath.first(response, "FI/ORG")
    @institution_name = x.nil? ? nil : x.text.strip
    x = REXML::XPath.first(response, "FI/FID")
    @institution_id = x.nil? ? nil :  x.text.strip
  end
end

class OfxStatement
  attr_reader(
    :currency,
    :account_from_bank_id,
    :account_id,
    :account_type,
    :ledger_balance,
    :ledger_balance_date,
    :transactions
  ) 
  
  def initialize(statement_type, statement)
    @currency = REXML::XPath.first(statement, "//CURDEF").text.strip
    from = ''
    case statement_type
    when :bank
      from = 'BANKACCTFROM'
    when :credit_card
      from = 'CCACCTFROM'
    else
      raise "undefined OFX statment type #{statement_type.to_s}"
    end
      
    @account_id = REXML::XPath.first(statement, "//#{from}/ACCTID").text.strip
    x = REXML::XPath.first(statement, "//#{from}/BANKID")
    @account_from_bank_id = x.nil? ? nil : x.text.strip
    x = REXML::XPath.first(statement, "//#{from}/ACCTTYPE")
    @account_type = x.nil? ? nil : x.text.strip

    @ledger_balance = REXML::XPath.first(statement, "//LEDGERBAL/BALAMT").text.strip
    @ledger_balance_date = OfxUtil.parse_date( REXML::XPath.first(statement, "//LEDGERBAL/DTASOF").text.strip )
    @transactions = []
    REXML::XPath.each(statement, "//BANKTRANLIST/STMTTRN"){|t|
      @transactions << OfxTransaction.new(t) 
    }
  end
end

class OfxTransaction
  attr_reader(
    :transaction_id,
    :transaction_type, 
    :amount,
    :date_posted,
    :name,
    :check_number,
    :correct_transaction_id,
    :correct_action
  )
  
  def initialize(tx)
    tx.elements.each do |e|
      case e.name
      when "TRNTYPE"
        @transaction_type = e.text.strip
      when "DTPOSTED"
        @date_posted = OfxUtil.parse_date(e.text.strip)
      when "TRNAMT"
        @amount = e.text.strip
      when "FITID"
        @transaction_id = e.text.strip
      when "CORRECTFITID"
        @correct_transaction_id = e.text.strip
      when "CORRECTACTION"
        @correct_action = e.text.strip
      when "CHECKNUM"
        @check_number = e.text.strip
      when "NAME"
        @name = e.text.strip
      when "DTUSER", "DTAVAIL", "FITSRVRTIDID", "SIC",
          "PAYEEID", "PAYEE", "BANKACCTTO", "CCACCTTO",
          "CURRENCY", "ORIGCURRENCY"
        # not handling these elements yet
      else
        raise "Unknown child element in Statement Transaction: #{e.name}"
      end
    end
  end
end

class OfxTransactionType
  #        Credit,
  #        Debit,
  #        Interest,
  #        Dividend,
  #        Fee,
  #        ServiceCharge,
  #        Deposit,
  #        ATM,
  #        PointOfSale,
  #        Transfer,
  #        Check,
  #        Payment,
  #        Cash,
  #        DirectDeposit,
  #        DirectDebit,
  #        RepeatPayment,
  #        Other,
  #        Invalid
end

class OfxUtil
  DATE_FORMAT = /^(\d{4})(\d{2})(\d{2})/
  
  def self.parse_date(ofx_date)
    if(m = DATE_FORMAT.match(ofx_date))
      return Date.new(m[1].to_i, m[2].to_i, m[3].to_i)
    else
      raise "Invalid OFX date: #{ofx_date}"
    end
  end
end

class Ofx
  attr_reader :header, :path, :signon_response, :statement

  HEADER = Regexp.new(".+?\n\n", Regexp::MULTILINE)
  BODY = Regexp.new("<ofx>.+?</ofx>", Regexp::MULTILINE | Regexp::IGNORECASE)
  AGGREGATE = Regexp.new("^</*.+?>$", Regexp::IGNORECASE)
  TAG = Regexp.new("<(.+?)>", Regexp::IGNORECASE)
  
  def initialize(path)
    @path = path
    ofx_text = ''
    File.open(@path) do |f|
      ofx_text = f.read
    end
    match = HEADER.match(ofx_text)
    throw "No valid OFX header found." if match.nil?
    @header = OfxHeader.new(match[0].split("\n"))
    
    match = BODY.match(ofx_text)
    throw "No valid OFX body found." if match.nil?
    parse_body(match[0])
  end
  
  def convert_sgml_to_xml(sgml)
    sgml.gsub!("<", "\n<")
    sgml_lines = sgml.split("\n").select{|x| !(x.nil? || "" == x) }
    xml = ""
    sgml_lines.each do |line|
      line.strip!
      next if '' == line
      if(AGGREGATE.match(line))
        xml << line
      else
        if(tm = TAG.match(line))
          xml << "#{line}</#{tm[1]}>"
        else
          raise "#{line.inspect} does not match TAG pattern"
        end
      end
    end
    return xml
  end
  
  def parse_body(body_sgml)
    body_xml = convert_sgml_to_xml(body_sgml)
    body = REXML::Document.new(body_xml)
    REXML::XPath.each(body, "//SIGNONMSGSRSV1/SONRS"){|x|
      @signon_response = OfxSignonResponse.new(x) 
    }
    t = REXML::XPath.first(body, "//CREDITCARDMSGSRSV1/CCSTMTTRNRS")
    @statement = OfxStatement.new(:credit_card, t) unless t.nil?
    t = REXML::XPath.first(body, "//BANKMSGSRSV1/STMTTRNRS")
    @statement = OfxStatement.new(:bank, t) unless t.nil?
  end
end
